﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using Scrabble.Model.Events;
using Scrabble.Model.Properties;

namespace Scrabble.Model
{
    public partial class Game : IGame
    {
        public const int MaxPlayers = 4;
        public const int MaxTilesInRack = 7;

        private readonly IRandomNumberSeries _sequence;
        private readonly List<Tile> _pool;
        private readonly Dictionary<BoardLocation, ScoringStyle> _premiumSquares = new Dictionary<BoardLocation, ScoringStyle>();
        private readonly Board _board = new Board();
        private readonly ObservableCollection<IGameEvent> _eventLog = new ObservableCollection<IGameEvent>();
        private readonly List<IPlayer> _setOfPlayers;
        private IPlayer _activePlayer;

        public event EventHandler ActivePlayerChanged;
        public event EventHandler PoolChanged;
        public event EventHandler GameOver;

        public Game(int seed, IEnumerable<string> playerNames)
        {
            Contract.Requires<ArgumentNullException>(playerNames != null);
            Contract.Requires<ArgumentException>(playerNames.Count() > 1 && playerNames.Count() <= MaxPlayers);

            _sequence = new RandomNumberSeries(seed);
            _pool = Tiles.Shuffle(_sequence.Next).ToList();
            _setOfPlayers = playerNames.Select(name => (IPlayer) new Player(this, name)).ToList();

            Guard.Against<InvalidOperationException>(_pool.Count == 100, "Incorrect number of tiles at start");

            foreach (var square in Board.PremiumSquares)
                _premiumSquares[square.Item1] = square.Item2;

            TimeKeeper = new BasicTimeKeeper(this);
            ActivePlayer = Players.First();
        }

        public ITimeKeeper TimeKeeper
        {
            get; set;
        }

        public Board Board
        {
            get { return _board; }
        }

        public int TotalPlayers
        {
            get { return _setOfPlayers.Count; }
        }

        public IEnumerable<IPlayer> Players
        {
            get { return _setOfPlayers; }
        }

        public IPlayer PreviousPlayer
        {
            get { return _setOfPlayers[(_setOfPlayers.IndexOf(ActivePlayer) + TotalPlayers - 1) % TotalPlayers]; }
        }

        public IPlayer ActivePlayer
        {
            get { return _activePlayer; }
            private set
            {
                if (value == ActivePlayer) return;

                _activePlayer = value;

                var handler = ActivePlayerChanged;
                if (handler != null) handler(this, EventArgs.Empty);
            }
        }

        public bool IsGameOver
        {
            get { return ActivePlayer == null || FirstPlayerToPlayAll != null || AllPlayersPassedInSuccession; }
        }

        public IEnumerable<IGameEvent> EventLog
        {
            get { return _eventLog; }
        }

        public IEnumerable<TileStatistics> TileUsage
        {
            get
            {
                return
                    from sample in TileDistribution
                    join grouping in _pool.GroupBy(tile => tile.Letter) on sample.Tile.Letter equals grouping.Key 
                    into outer
                        from grouping in outer.DefaultIfEmpty()
                        select new TileStatistics
                        {
                            Letter = sample.Tile.Letter,
                            TotalInGame = sample.Quantity,
                            TotalInPool = grouping == null ? 0 : grouping.Count()
                        };
            }
        }

        public bool IsPlacementValid(PlayVector at)
        {
            if (at == null || !at.StartingLocation.IsWithinBounds) return false;

            if ((at.Length == 1) &&
                (at.IsAcross 
                    ? at.StartingLocation.NeighbouringLocationsAcross 
                    : at.StartingLocation.NeighbouringLocationsDown).All(location => !Board.TileExistsAt(location)))
                return false;

            var placementLocations = Board.AvailableLocationsIn(at).Take(at.Length);

            return Board.IsEmpty
                ? placementLocations.Any(location => location == Board.CenterLocation)
                : placementLocations.SelectMany(location => location.NeighbouringLocations).Any(neighbouringLocation => Board.TileExistsAt(neighbouringLocation));
        }

        public int ScorePlacement(IEnumerable<Tile> tiles, PlayVector at)
        {
            return SetOfWordsFormedBy(tiles, at).Aggregate(tiles.Count() == MaxTilesInRack ? 50 : 0, (result, word) => result + ScorePlacement(word));
        }

        public IEnumerable<IList<TileInPlay>> SetOfWordsFormedBy(IEnumerable<Tile> tiles, PlayVector at)
        {
            var setOfWords = new List<IList<TileInPlay>> { FormWordAt(at.StartingLocation, tiles, at.Direction) };

            setOfWords.AddRange(
                (from part in setOfWords.First()
                 join location in Board.AvailableLocationsIn(at).Take(tiles.Count()) on part.Location equals location
                 select FormWordAt(location, Enumerable.Repeat(part.Tile, 1), at.PerpendicularDirection)).Where(setOfTiles => setOfTiles.Count > 1));

            return setOfWords;
        }

        #region Implementation

        private IPlayer FirstPlayerToPlayAll
        {
            get; set;
        }

        private bool AllPlayersPassedInSuccession
        {
            get
            {
                var totalPlayers = TotalPlayers;
                var earlierPlays = _eventLog.Count - totalPlayers;

                return earlierPlays >= 0 && _eventLog.Skip(earlierPlays).OfType<SkippedTurnEvent>().Count() == totalPlayers;
            }
        }

        private bool IsFinalRoundComplete
        {
            get
            {
                if (FirstPlayerToPlayAll == null) return false;

                var playsSinceFirstEmptyRackOccurred = _eventLog.Count - _setOfPlayers.IndexOf(FirstPlayerToPlayAll);
                return (TotalPlayers == 1) || (playsSinceFirstEmptyRackOccurred > 0 && playsSinceFirstEmptyRackOccurred % TotalPlayers == 0);
            }
        }

        private void ExchangeTiles(ICollection<Tile> rack, IEnumerable<Tile> tilesToExchange)
        {
            var totalTiles = tilesToExchange.Count();

            Guard.Against<InvalidOperationException>(rack.Intersect(tilesToExchange).Count() != totalTiles, "Error: attempting to play unknown tiles.");

            Guard.Against<InvalidMoveException>(_pool.Count < MaxTilesInRack, Resources.Prompt_ExchangeNotAllowed);
            Guard.Against<InvalidMoveException>(_pool.Count < totalTiles, Resources.Prompt_InsufficientTilesInPoolForExchange);

            // Remove old tiles from rack
            foreach (var tile in tilesToExchange)
            {
                Debug.Assert(rack.Contains(tile));
                rack.Remove(tile);
            }

            // Add new tiles from pool
            FillRack(rack);

            // Return old tiles to pool
            foreach (var tile in tilesToExchange)
            {
                _pool.Insert(_sequence.Next(_pool.Count), tile);
            }

            NotifyOfPoolChange();
        }

        private void PlaceTiles(ICollection<Tile> rack, IEnumerable<TileInPlay> setOfPlays)
        {
            foreach (var play in setOfPlays)
            {
                Contract.Assert(rack.Contains(play.Tile));
                rack.Remove(play.Tile);

                Contract.Assert(!_board.TileExistsAt(play.Location));
                _board[play.Location] = play.Tile;

                _premiumSquares.Remove(play.Location);
            }

            FillRack(rack);
            NotifyOfPoolChange();
        }

        private void GoToNextPlayer(GameEvent gameEvent)
        {
            Contract.Requires(gameEvent != null);

            if (ActivePlayer == null) return;

            gameEvent.DurationOfPlay = TimeKeeper != null ? TimeKeeper.TimeInPlay : TimeSpan.Zero;
            _eventLog.Add(gameEvent);

            if (IsGameOver)
            {
                ActivePlayer = null;

                var deductions = _setOfPlayers
                    .Where(player => player.CurrentTiles.Any())
                    .Select(player => new PointsDeductedEvent(player, player.CurrentTiles));

                _eventLog.AddRange(deductions);

                if (FirstPlayerToPlayAll != null && deductions.Any())
                {
                    var totalDeducted = deductions.Aggregate(0, (result, deduction) => result + deduction.Score);
                    _eventLog.Add(new FirstToEndBonusEvent(FirstPlayerToPlayAll, -totalDeducted));
                }

                var handler = GameOver;
                if (handler != null) handler(this, EventArgs.Empty);
            }
            else
            {
                ActivePlayer = _setOfPlayers[(_setOfPlayers.IndexOf(ActivePlayer) + 1) % TotalPlayers];
            }
        }

        private void FillRack(ICollection<Tile> rack)
        {
            Contract.Requires<ArgumentNullException>(rack != null);

            if (_pool.Any())
            {
                while (rack.Count < MaxTilesInRack && _pool.Count > 0)
                    rack.Add(RandomTileFromPool());
            }
            else if (FirstPlayerToPlayAll == null && ! rack.Any())
            {
                FirstPlayerToPlayAll = ActivePlayer;
            }
        }

        private Tile RandomTileFromPool()
        {
            Contract.Requires<InvalidOperationException>(_pool.Count > 0);

            var next = _sequence.Next(_pool.Count);
            var tile = _pool[next];

            _pool.RemoveAt(next);
            return tile;
        }

        private IList<TileInPlay> FormWordAt(BoardLocation startingLocation, IEnumerable<Tile> followingTiles, PlayDirection direction)
        {
            var word = (direction == PlayDirection.Across ? startingLocation.PrecedingLocations : startingLocation.PrecedingLocationsUp)
                .TakeWhile(location => Board.TileExistsAt(location))
                .Reverse()
                .Select(location => new TileInPlay(Board.TileAt(location), location))
                .ToList();

            var followingLocations = Enumerable.Repeat(startingLocation, 1)
                .Concat(direction == PlayDirection.Across ? startingLocation.FollowingLocationsAcross : startingLocation.FollowingLocationsDown);

            var nextTile = followingTiles.GetEnumerator();

            foreach (var location in followingLocations)
            {
                var tile = Board.TileAt(location);

                if (tile == null)
                {
                    if (!nextTile.MoveNext()) break;
                    tile = nextTile.Current;
                }

                word.Add(new TileInPlay(tile, location));
            }

            return word;
        }

        private void NotifyOfPoolChange()
        {
            var handler = PoolChanged;
            if (handler != null) handler(this, EventArgs.Empty);
        }

        #endregion
    }
}
